﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ServiceModel;
using DeepTalkRenderServiceLib.DTRenderService;
using System.Threading;
using System.Diagnostics;

namespace DeepTalkRenderServiceLib
{
    /// <summary>
    /// Renders a single meeting.
    /// </summary>
    public class RenderMeetingTask
    {
        public static Task Start(RenderCallbackFunctionType callback, string wcfRenderContractName)
        {
            var a = new RenderMeetingTask(wcfRenderContractName);
            a.RenderCallback = callback;
            return Task.Factory.StartNew(() => a.RenderOneMeeting());
        }

        private string _wcfRenderContractName;

        /// <summary>
        /// Init the monitor - and make sure we have a contract endpoint name.
        /// Force the user to specify it - we really can't run w/out it.
        /// </summary>
        /// <param name="wcfRenderContractName"></param>
        public RenderMeetingTask(string wcfRenderContractName)
        {
            _wcfRenderContractName = wcfRenderContractName;
        }

        /// <summary>
        /// This clas contains data on the rendering that we need to pass back.
        /// </summary>
        public class RenderResultsInfo
        {
            /// <summary>
            /// How big in MB is the final render (not really used).
            /// </summary>
            public float SizeOfRender { get; set; }

            /// <summary>
            /// How many talks were there?
            /// </summary>
            public int NumberOfTalks { get; set; }

            /// <summary>
            /// How long before next update. No value if no further updates
            /// are required.
            /// </summary>
            public TimeSpan? HowLongToNextUpdate { get; set; }

            /// <summary>
            /// Titile fo the Meeting
            /// </summary>
            public string Title { get; set; }

            /// <summary>
            /// When the meeting starts
            /// </summary>
            public DateTime MeetingStart { get; set; }

            /// <summary>
            /// When the meeting ends
            /// </summary>
            public DateTime MeetingEnd { get; set; }
        }

        /// <summary>
        /// Callback object that gets called when a meeting is ready to be rendered.
        /// </summary>
        class CallBackObject : IDTRenderCallback
        {
            private RenderMeetingTask renderMeetingTask;

            public CallBackObject(RenderMeetingTask renderMeetingTask)
            {
                this.renderMeetingTask = renderMeetingTask;
            }
            public bool RenderTalk(DTRenderTalkInfo info)
            {
                Trace.WriteLine("CallBackObject::RenderTalk - got request to render " + info.ID);
                Task.Factory.StartNew(() => renderMeetingTask.RenderTalk(info));
                return true;
            }
        }

        /// <summary>
        /// Keep track of the running render.
        /// </summary>
        private AutoResetEvent _renderDone = new AutoResetEvent(false);

        /// <summary>
        /// Callback function for a render guy
        /// </summary>
        /// <param name="?"></param>
        /// <returns></returns>
        public delegate RenderResultsInfo RenderCallbackFunctionType (string agendaXML, int talkID);

        /// <summary>
        /// Callback that does the actual work.
        /// </summary>
        private RenderCallbackFunctionType RenderCallback {get; set;}

        /// <summary>
        /// Keep a timer that, when it fires, will do periodic updates in the main
        /// databaes. This make sure that, if we crash, this guy will be re-rendered. Sure, that
        /// could cause an infinite loop, but lets hope not...
        /// </summary>
        private Timer _updateRenderTimeInDB;

        /// <summary>
        /// We do and manage the actual render here.
        /// 1. Make sure the call-back gets the render and gets started.
        /// 2. Make sure to keep the next time, etc., stuff up to date as we make (or don't make) progress.
        /// 3. When done update the database with all the correct information.
        /// </summary>
        /// <param name="info"></param>
        private void RenderTalk(DTRenderTalkInfo info)
        {
            Trace.WriteLine("RenderMeetingTask::RenderTalk - Doing actual rendering of the talk " + info.ID);

            RenderResultsInfo results = null;

            try
            {
                try
                {
                    ///
                    /// Set the initial update time
                    /// 

                    _renderClient.NextUpdateTime(info.ID, DateTime.Now + TimeSpan.FromMinutes(60));

                    ///
                    /// This timer keeps the main database up to date with the next-render time.
                    ///

                    _updateRenderTimeInDB = new Timer(o =>
                    {
                        try
                        {
                            Trace.WriteLine("RenderMeetingTask::RenderTalk Going to update the next render time via timer");
                            _renderClient.NextUpdateTime(info.ID, DateTime.Now + TimeSpan.FromMinutes(60));
                        }
                        catch (Exception e)
                        {
                            Trace.WriteLine("Error caught while trying to update the database with the next render time info: " + e.Message + " - " + e.StackTrace);
                            if (e.InnerException != null)
                            {
                                Trace.WriteLine("  - Inner Exception: " + e.InnerException.Message + " - " + e.InnerException.StackTrace);
                            }
                        }
                    },
                    null,
                    0,
                    5 * 60 * 1000);

                    /// Do the actual work. Don't return here until done with it! :-)
                    results = RenderCallback(info.AgendaXML, info.ID);

                }
                catch (Exception e)
                {
                    Trace.WriteLine("RenderMeetingTask::RenderTalk: Error while doing actual render: " + e.Message + " - " + e.StackTrace);
                }

                ///
                /// We are done! Next task is to shut off the timer before anything can be updated.
                /// We use this other form of the auto reset event becasue we need to make sure that there are 
                /// no timer calls going when we complete the reset of this run - otherwise they may come in and try to
                /// use the _renderClient after it has been disconnected. Ugh! -)
                /// 

                {
                    AutoResetEvent reset = new AutoResetEvent(false);
                    _updateRenderTimeInDB.Dispose(reset);
                    reset.WaitOne();
                    _updateRenderTimeInDB = null;
                }

                ///
                /// Notify home we are finished, and update with last info, etc.
                /// 

                try
                {
                    if (results != null)
                    {
                        if (!results.HowLongToNextUpdate.HasValue)
                        {
                            _renderClient.NoFurtherUpdates(info.ID);
                        }
                        else
                        {
                            _renderClient.NextUpdateTime(info.ID, DateTime.Now + results.HowLongToNextUpdate.Value);
                        }

                        ///
                        /// Update the meta data and then we are outta here
                        /// 

                        _renderClient.UpdateConferenceData(info.ID, results.Title, results.MeetingStart, results.MeetingEnd);
                        _renderClient.RenderDone(info.ID, results.SizeOfRender, results.NumberOfTalks);
                    }

                }
                catch (Exception e)
                {
                    Trace.WriteLine("RenderMeetingTask::RenderTalk: Error while finalizing render: " + e.Message + " - " + e.StackTrace);
                }

            }
            finally
            {
                ////
                /// Notify the rest of the world that all is good!
                /// 

                _renderDone.Set();
            }
        }

        /// <summary>
        /// What we are using to talk to the database service. has to be kept open...
        /// </summary>
        private DTRenderClient _renderClient = null;

        /// <summary>
        /// We connect and render a single meeting. We just tell the render dude we are ready, and then wait
        /// for the render to finish (on other task/threads).
        /// </summary>
        /// <returns></returns>
        private void RenderOneMeeting()
        {
            Trace.WriteLine("RenderMeetingTask: Task is starting");
            try
            {
                var cbo = new CallBackObject(this);
                using (_renderClient = new DTRenderClient(new InstanceContext(cbo), _wcfRenderContractName))
                {
                    _renderClient.Open(); // Use the open because the failure message makes more sense than the timeout message below.
                    Trace.WriteLine("Informing that we are ready for a talk to render!");
                    _renderClient.ReadyForWork();
                    _renderDone.WaitOne();
                    Trace.WriteLine("RenderMeetingTask::RenderOneMeeting Disposing of the render client object");
                }
            }
            catch (Exception e)
            {
                Trace.WriteLine("RenderMeetingTalks: Error while ready for work and waiting: " + e.Message + " - " + e.StackTrace);
            }
        }
    }
}
